"git2-curl 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
"glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)",
"hamcrest 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
+ "handlebars 0.20.5 (registry+https://github.com/rust-lang/crates.io-index)",
"kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)",
"libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)",
"libgit2-sys 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)",
"regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "handlebars"
+version = "0.20.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+dependencies = [
+ "lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)",
+ "log 0.3.6 (registry+https://github.com/rust-lang/crates.io-index)",
+ "pest 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)",
+ "quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)",
+ "regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)",
+ "rustc-serialize 0.3.21 (registry+https://github.com/rust-lang/crates.io-index)",
+]
+
[[package]]
name = "idna"
version = "0.1.0"
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "lazy_static"
+version = "0.1.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "lazy_static"
version = "0.2.2"
"user32-sys 0.2.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "pest"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "pkg-config"
version = "0.3.8"
"winapi-build 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)",
]
+[[package]]
+name = "quick-error"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+
[[package]]
name = "rand"
version = "0.3.14"
"checksum git2-curl 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)" = "68676bc784bf0bef83278898929bf64a251e87c0340723d0b93fa096c9c5bf8e"
"checksum glob 0.2.11 (registry+https://github.com/rust-lang/crates.io-index)" = "8be18de09a56b60ed0edf84bc9df007e30040691af7acd1c41874faac5895bfb"
"checksum hamcrest 0.1.1 (registry+https://github.com/rust-lang/crates.io-index)" = "bf088f042a467089e9baa4972f57f9247e42a0cc549ba264c7a04fbb8ecb89d4"
+"checksum handlebars 0.20.5 (registry+https://github.com/rust-lang/crates.io-index)" = "07f9c1d28bcfb97143c95ed0667141677b2b5675c7ba3d5b81459ad43b1073bd"
"checksum idna 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "1053236e00ce4f668aeca4a769a09b3bf5a682d802abd6f3cb39374f6b162c11"
"checksum kernel32-sys 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+"checksum lazy_static 0.1.16 (registry+https://github.com/rust-lang/crates.io-index)" = "cf186d1a8aa5f5bee5fd662bc9c1b949e0259e1bcc379d1f006847b0080c7417"
"checksum lazy_static 0.2.2 (registry+https://github.com/rust-lang/crates.io-index)" = "6abe0ee2e758cd6bc8a2cd56726359007748fbf4128da998b65d0b70f881e19b"
"checksum libc 0.2.18 (registry+https://github.com/rust-lang/crates.io-index)" = "a51822fc847e7a8101514d1d44e354ba2ffa7d4c194dcab48870740e327cac70"
"checksum libgit2-sys 0.6.5 (registry+https://github.com/rust-lang/crates.io-index)" = "502e50bcdcfa98df366bdd54935bff856f4cf11f725daa608092c0288205887a"
"checksum openssl 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "1eb2a714828f5528e4a24a07c296539216f412364844d61fe1161f94558455d4"
"checksum openssl-probe 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "756d49c8424483a3df3b5d735112b4da22109ced9a8294f1f5cdf80fb3810919"
"checksum openssl-sys 0.9.1 (registry+https://github.com/rust-lang/crates.io-index)" = "95e9fb08acc32509fac299d6e5f4932e1e055bb70d764282c3ed8beaa87ab0e9"
+"checksum pest 0.3.3 (registry+https://github.com/rust-lang/crates.io-index)" = "0a6dda33d67c26f0aac90d324ab2eb7239c819fc7b2552fe9faa4fe88441edc8"
"checksum pkg-config 0.3.8 (registry+https://github.com/rust-lang/crates.io-index)" = "8cee804ecc7eaf201a4a207241472cc870e825206f6c031e3ee2a72fa425f2fa"
"checksum psapi-sys 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "abcd5d1a07d360e29727f757a9decb3ce8bc6e0efa8969cfaad669a8317a2478"
+"checksum quick-error 1.1.0 (registry+https://github.com/rust-lang/crates.io-index)" = "0aad603e8d7fb67da22dbdf1f4b826ce8829e406124109e73cf1b2454b93a71c"
"checksum rand 0.3.14 (registry+https://github.com/rust-lang/crates.io-index)" = "2791d88c6defac799c3f20d74f094ca33b9332612d9aef9078519c82e4fe04a5"
"checksum regex 0.1.80 (registry+https://github.com/rust-lang/crates.io-index)" = "4fd4ace6a8cf7860714a2c2280d6c1f7e6a413486c13298bbc86fd3da019402f"
"checksum regex-syntax 0.3.9 (registry+https://github.com/rust-lang/crates.io-index)" = "f9ec002c35e86791825ed294b50008eea9ddfc8def4420124fbc6b08db834957"
git2 = "0.6"
git2-curl = "0.7"
glob = "0.2"
+handlebars = "0.20"
libc = "0.2"
libgit2-sys = "0.6"
log = "0.3"
+miow = "0.1"
num_cpus = "1.0"
regex = "0.1"
rustc-serialize = "0.3"
flag_lib: bool,
arg_path: Option<String>,
flag_name: Option<String>,
+ flag_template_subdir: Option<String>,
+ flag_template: Option<String>,
flag_vcs: Option<ops::VersionControl>,
flag_frozen: bool,
flag_locked: bool,
--bin Use a binary (application) template
--lib Use a library template
--name NAME Set the resulting package name
+ --template <repository> Use a specified template repository
+ --template-subdir <template> Use a specified template within a template repository
-v, --verbose ... Use verbose output (-vv very verbose/build.rs output)
-q, --quiet No output printed to stdout
--color WHEN Coloring: auto, always, never
options.flag_frozen,
options.flag_locked)?;
- let Options { flag_bin, flag_lib, arg_path, flag_name, flag_vcs, .. } = options;
+ let Options {
+ flag_bin, flag_lib,
+ arg_path, flag_name,
+ flag_vcs,
+ flag_template_subdir, flag_template,
+ ..
+ } = options;
let tmp = &arg_path.unwrap_or(format!("."));
let opts = ops::NewOptions::new(flag_vcs,
flag_bin,
flag_lib,
tmp,
- flag_name.as_ref().map(|s| s.as_ref()));
+ flag_name.as_ref().map(|s| s.as_ref()),
+ flag_template_subdir.as_ref().map(|s| s.as_ref()),
+ flag_template.as_ref().map(|s| s.as_ref()));
let opts_lib = opts.lib;
ops::init(opts, config)?;
flag_lib: bool,
arg_path: String,
flag_name: Option<String>,
+ flag_template_subdir: Option<String>,
+ flag_template: Option<String>,
flag_vcs: Option<ops::VersionControl>,
flag_frozen: bool,
flag_locked: bool,
--bin Use a binary (application) template
--lib Use a library template
--name NAME Set the resulting package name, defaults to the value of <path>
+ --template <repository> Use a specified template repository
+ --template-subdir <template-subdir> Use a specified template within a template repository
-v, --verbose ... Use verbose output (-vv very verbose/build.rs output)
-q, --quiet No output printed to stdout
--color WHEN Coloring: auto, always, never
options.flag_frozen,
options.flag_locked)?;
- let Options { flag_bin, flag_lib, arg_path, flag_name, flag_vcs, .. } = options;
+ let Options {
+ flag_bin, flag_lib,
+ arg_path, flag_name,
+ flag_vcs,
+ flag_template_subdir, flag_template,
+ ..
+ } = options;
let opts = ops::NewOptions::new(flag_vcs,
flag_bin,
flag_lib,
&arg_path,
- flag_name.as_ref().map(|s| s.as_ref()));
+ flag_name.as_ref().map(|s| s.as_ref()),
+ flag_template_subdir.as_ref().map(|s| s.as_ref()),
+ flag_template.as_ref().map(|s| s.as_ref()));
let opts_lib = opts.lib;
ops::new(opts, config)?;
extern crate fs2;
extern crate git2;
extern crate glob;
+extern crate handlebars;
extern crate libc;
extern crate libgit2_sys;
extern crate num_cpus;
use std::env;
-use std::fs;
-use std::path::Path;
+use std::fs::{self, DirEntry, File};
+use std::path::{Path, PathBuf};
use std::collections::BTreeMap;
-
use rustc_serialize::{Decodable, Decoder};
use git2::Config as GitConfig;
use term::color::BLACK;
+use handlebars::{Handlebars, Context, no_escape};
+use tempdir::TempDir;
+
use core::Workspace;
+use sources::git::clone;
use util::{GitRepo, HgRepo, CargoResult, human, ChainError, internal};
-use util::{Config, paths};
-
-use toml;
+use util::{Config, paths, template};
+use util::template::{TemplateSet, TemplateFile, TemplateDirectory, TemplateType};
+use util::template::{InputFileTemplateFile, InMemoryTemplateFile, get_template_type};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum VersionControl { Git, Hg, NoVcs }
pub lib: bool,
pub path: &'a str,
pub name: Option<&'a str>,
+ pub template_subdir: Option<&'a str>,
+ pub template: Option<&'a str>,
}
struct SourceFileInformation {
struct MkOptions<'a> {
version_control: Option<VersionControl>,
+ template_subdir: Option<&'a str>,
+ template: Option<&'a str>,
path: &'a Path,
name: &'a str,
- source_files: Vec<SourceFileInformation>,
bin: bool,
}
bin: bool,
lib: bool,
path: &'a str,
- name: Option<&'a str>) -> NewOptions<'a> {
+ name: Option<&'a str>,
+ template_subdir: Option<&'a str>,
+ template: Option<&'a str>) -> NewOptions<'a> {
// default to lib
let is_lib = if !bin {
lib: is_lib,
path: path,
name: name,
+ template_subdir: template_subdir,
+ template: template,
}
}
}
version_control: Option<VersionControl>,
}
+fn get_input_template(config: &Config, opts: &MkOptions) -> CargoResult<TemplateSet> {
+ let name = opts.name;
+
+ let template_type = try!(get_template_type(opts.template, opts.template_subdir));
+ let template_set = match template_type {
+ // given template is a remote git repository & needs to be cloned
+ TemplateType::GitRepo(repo_url) => {
+ let template_dir = try!(TempDir::new(name));
+ config.shell().status("Cloning", &repo_url)?;
+ clone(&repo_url, &template_dir.path(), &config)?;
+ let template_path = find_template_subdir(&template_dir.path(), opts.template_subdir);
+ TemplateSet {
+ template_dir: Some(TemplateDirectory::Temp(template_dir)),
+ template_files: try!(collect_template_dir(&template_path, opts.path))
+ }
+ },
+ // given template is a local directory
+ TemplateType::LocalDir(directory) => {
+ // make sure that the template exists
+ if fs::metadata(&directory).is_err() {
+ bail!("template `{}` not found", directory);
+ }
+ let template_path = find_template_subdir(&PathBuf::from(&directory),
+ opts.template_subdir);
+ TemplateSet {
+ template_dir: Some(TemplateDirectory::Normal(PathBuf::from(directory))),
+ template_files: try!(collect_template_dir(&template_path, opts.path))
+ }
+ },
+ // no template given, use either "lib" or "bin" templates depending on the
+ // presence of the --bin flag.
+ TemplateType::Builtin => {
+ let template_files = if opts.bin {
+ create_bin_template()
+ } else {
+ create_lib_template()
+ };
+ TemplateSet {
+ template_dir: None,
+ template_files: template_files
+ }
+ }
+ };
+ Ok(template_set)
+}
+
fn get_name<'a>(path: &'a Path, opts: &'a NewOptions, config: &Config) -> CargoResult<&'a str> {
if let Some(name) = opts.name {
return Ok(name);
let mkopts = MkOptions {
version_control: opts.version_control,
+ template_subdir: opts.template_subdir,
+ template: opts.template,
path: &path,
name: name,
- source_files: vec![plan_new_source_file(opts.bin, name.to_string())],
bin: opts.bin,
};
let mkopts = MkOptions {
version_control: version_control,
+ template_subdir: opts.template_subdir,
+ template: opts.template,
path: &path,
name: name,
bin: src_paths_types.iter().any(|x|x.bin),
- source_files: src_paths_types,
};
mk(config, &mkopts).chain_error(|| {
let (author_name, email) = discover_author()?;
// Hoo boy, sure glad we've got exhaustivenes checking behind us.
- let author = match (cfg.name, cfg.email, author_name, email) {
+ let author = match (cfg.name.clone(), cfg.email.clone(), author_name, email) {
(Some(name), Some(email), _, _) |
(Some(name), None, _, Some(email)) |
(None, Some(email), name, _) |
(None, None, name, None) => name,
};
- let mut cargotoml_path_specifier = String::new();
-
- // Calculare what [lib] and [[bin]]s do we need to append to Cargo.toml
-
- for i in &opts.source_files {
- if i.bin {
- if i.relative_path != "src/main.rs" {
- cargotoml_path_specifier.push_str(&format!(r#"
-[[bin]]
-name = "{}"
-path = {}
-"#, i.target_name, toml::Value::String(i.relative_path.clone())));
- }
- } else {
- if i.relative_path != "src/lib.rs" {
- cargotoml_path_specifier.push_str(&format!(r#"
-[lib]
-name = "{}"
-path = {}
-"#, i.target_name, toml::Value::String(i.relative_path.clone())));
- }
- }
- }
-
- // Create Cargo.toml file with necessary [lib] and [[bin]] sections, if needed
-
- paths::write(&path.join("Cargo.toml"), format!(
-r#"[package]
-name = "{}"
-version = "0.1.0"
-authors = [{}]
-
-[dependencies]
-{}"#, name, toml::Value::String(author), cargotoml_path_specifier).as_bytes())?;
-
-
- // Create all specified source files
- // (with respective parent directories)
- // if they are don't exist
-
- for i in &opts.source_files {
- let path_of_source_file = path.join(i.relative_path.clone());
-
- if let Some(src_dir) = path_of_source_file.parent() {
- fs::create_dir_all(src_dir)?;
+ // construct the mapping used to populate the template
+ // if in the future we want to make more varaibles available in
+ // the templates, this would be the place to do it.
+ let mut handlebars = Handlebars::new();
+ // We don't want html escaping unless users explicitly ask for it...
+ handlebars.register_escape_fn(no_escape);
+ handlebars.register_helper("toml-escape", Box::new(template::toml_escape_helper));
+ handlebars.register_helper("html-escape", Box::new(template::html_escape_helper));
+
+ let mut data = BTreeMap::new();
+ data.insert("name".to_owned(), name.to_owned());
+ data.insert("author".to_owned(), author);
+
+ let template_set = try!(get_input_template(config, opts));
+ for template in template_set.template_files.iter() {
+ let template_str = try!(template.template());
+ let dest_path = path.join(template.path());
+
+ // Skip files that already exist.
+ if fs::metadata(&dest_path).is_ok() {
+ continue;
}
- let default_file_content : &[u8] = if i.bin {
- b"\
-fn main() {
- println!(\"Hello, world!\");
-}
-"
- } else {
- b"\
-#[cfg(test)]
-mod tests {
- #[test]
- fn it_works() {
- }
-}
-"
- };
-
- if !fs::metadata(&path_of_source_file).map(|x| x.is_file()).unwrap_or(false) {
- paths::write(&path_of_source_file, default_file_content)?;
- }
+ let parent = try!(dest_path.parent()
+ .chain_error(|| {
+ human(format!("failed to make sure parent directory \
+ exists for {}", dest_path.display()))
+ }));
+ try!(fs::create_dir_all(&parent)
+ .chain_error(|| {
+ human(format!("failed to create path to destination file {}",
+ parent.display()))
+ }));
+
+ // create the new file & render the template to it
+ let mut dest_file = try!(File::create(&dest_path).chain_error(|| {
+ human(format!("failed to open file for writing: {}",
+ dest_path.display()))
+ }));
+
+ try!(handlebars.template_renderw(&template_str, &Context::wraps(&data), &mut dest_file)
+ .chain_error(|| {
+ human(format!("Failed to render template for file: {}", dest_path.display()))
+ }))
}
if let Err(e) = Workspace::new(&path.join("Cargo.toml"), config) {
workspace configuration\n\n{}", e);
config.shell().warn(msg)?;
}
-
Ok(())
}
+// When the command line has --template=<repository-or-directory> and
+// --template-subdir=<template-name> then find_template_subdir fixes up the name as appropriate.
+fn find_template_subdir(template_dir: &Path, template: Option<&str>) -> PathBuf {
+ match template {
+ Some(template) => template_dir.join(template),
+ None => template_dir.to_path_buf()
+ }
+}
+
+fn collect_template_dir(template_path: &PathBuf, _: &Path) -> CargoResult<Vec<Box<TemplateFile>>> {
+ let mut templates = Vec::<Box<TemplateFile>>::new();
+ // For every file found inside the given template directory, compile it as a handlebars
+ // template and render it with the above data to a new file inside the target directory
+ try!(walk_template_dir(&template_path, &mut |entry| {
+ let entry_path = entry.path();
+ let dest_file_name = PathBuf::from(try!(entry_path.strip_prefix(&template_path)
+ .chain_error(|| {
+ human(format!("entry is somehow not a subpath \
+ of the directory being walked."))
+ })));
+ templates.push(Box::new(InputFileTemplateFile::new(entry_path,
+ dest_file_name.to_path_buf())));
+ Ok(())
+ }));
+ Ok(templates)
+}
+
fn get_environment_variable(variables: &[&str] ) -> Option<String>{
variables.iter()
.filter_map(|var| env::var(var).ok())
}
None => None
};
+
Ok(CargoNewConfig {
name: name,
email: email,
})
}
+/// Recursively list directory contents under `dir`, only visiting files.
+///
+/// This will also filter out files & files types which we don't want to
+/// try generate templates for. Image files, for instance.
+///
+/// It also filters out certain files & file types, as we don't want t
+///
+/// We use this instead of std::fs::walk_dir as it is marked as unstable for now
+///
+/// This is a modified version of the example at:
+/// http://doc.rust-lang.org/std/fs/fn.read_dir.html
+fn walk_template_dir(dir: &Path, cb: &mut FnMut(DirEntry) -> CargoResult<()>) -> CargoResult<()> {
+ let attr = try!(fs::metadata(&dir));
+ let ignore_files = vec![".gitignore"];
+
+ if !attr.is_dir() {
+ return Ok(());
+ }
+ for entry in try!(fs::read_dir(dir)) {
+ let entry = try!(entry);
+ let attr = try!(fs::metadata(&entry.path()));
+ if attr.is_dir() {
+ if let Some(ref path_str) = entry.path().to_str() {
+ if !&path_str.contains(".git") {
+ try!(walk_template_dir(&entry.path(), cb));
+ }
+ }
+ } else {
+ if let Some(file_name) = entry.path().file_name() {
+ if ignore_files.contains(&file_name.to_str().unwrap()) {
+ continue
+ }
+ }
+ try!(cb(entry));
+ }
+ }
+ Ok(())
+}
+
+/// Create a generic template
+///
+/// This consists of a Cargo.toml, and a src directory.
+fn create_generic_template() -> Vec<Box<TemplateFile>> {
+ let template_file = Box::new(InMemoryTemplateFile::new(PathBuf::from("Cargo.toml"),
+ String::from(r#"[package]
+name = "{{name}}"
+version = "0.1.0"
+authors = [{{toml-escape author}}]
+
+[dependencies]
+"#)));
+ vec![template_file]
+}
+
+/// Create a new "lib" project
+fn create_lib_template() -> Vec<Box<TemplateFile>> {
+ let mut template_files = create_generic_template();
+ let lib_file = Box::new(InMemoryTemplateFile::new(PathBuf::from("src/lib.rs"),
+ String::from(r#"#[test]
+fn it_works() {
+}
+"#)));
+ template_files.push(lib_file);
+ template_files
+}
+
+/// Create a new "bin" project
+fn create_bin_template() -> Vec<Box<TemplateFile>> {
+ let mut template_files = create_generic_template();
+ let main_file = Box::new(InMemoryTemplateFile::new(PathBuf::from("src/main.rs"),
+String::from("fn main() {
+ println!(\"Hello, world!\");
+}
+")));
+ template_files.push(main_file);
+ template_files
+}
+
#[cfg(test)]
mod tests {
use super::strip_rust_affixes;
-pub use self::utils::{GitRemote, GitDatabase, GitCheckout, GitRevision, fetch};
+pub use self::utils::{GitRemote, GitDatabase, GitCheckout, GitRevision, fetch, clone};
pub use self::source::{GitSource, canonicalize_url};
mod utils;
mod source;
Ok(())
})
}
+
+/// Clone a remote repository into a target directory. This is a simple utility function to get
+/// HEAD. When this is complete it should be equivalent to `git clone $url $target`
+pub fn clone(url: &str, target: &Path, config: &Config) -> CargoResult<()> {
+ let repo = git2::Repository::init(target).chain_error(||{
+ human(format!("Failed to create template directory `{}`",
+ target.display()))
+ })?;
+ let refspec = "refs/heads/*:refs/heads/*";
+ fetch(&repo, &url, refspec, &config).chain_error(||{
+ human(format!("failed to fecth `{}`", url))
+ })?;
+ let reference = "HEAD";
+ let oid = repo.refname_to_id(reference)?;
+ let object = repo.find_object(oid, None)?;
+ repo.reset(&object, git2::ResetType::Hard, None)?;
+ Ok(())
+}
use std::fmt;
use std::io;
use std::num;
+use std::path;
use std::process::{Output, ExitStatus};
use std::str;
use std::string;
use curl;
use git2;
+use handlebars;
use rustc_serialize::json;
use semver;
use term;
term::Error,
num::ParseIntError,
str::ParseBoolError,
+ path::StripPrefixError,
+ handlebars::TemplateRenderError,
+ handlebars::RenderError,
}
impl From<string::ParseError> for Box<CargoError> {
impl CargoError for term::Error {}
impl CargoError for num::ParseIntError {}
impl CargoError for str::ParseBoolError {}
+impl CargoError for path::StripPrefixError {}
+impl CargoError for handlebars::TemplateRenderError {}
+impl CargoError for handlebars::RenderError {}
// =============================================================================
// Construction helpers
pub mod paths;
pub mod process_builder;
pub mod profile;
+pub mod template;
pub mod to_semver;
pub mod to_url;
pub mod toml;
use std::ffi::{OsStr, OsString};
use std::fs::File;
use std::fs::OpenOptions;
+use std::io;
use std::io::prelude::*;
use std::path::{Path, PathBuf, Component};
}
}
+pub fn file(p: &Path, contents: &[u8]) -> io::Result<()> {
+ try!(File::create(p)).write_all(contents)
+}
+
pub fn read(path: &Path) -> CargoResult<String> {
(|| -> CargoResult<_> {
let mut ret = String::new();
--- /dev/null
+use std::path::{Path, PathBuf};
+use std::fs::File;
+use std::io::{Read};
+
+use util::{CargoResult, human, ChainError};
+use url::Url;
+
+use handlebars::{Context, Helper, Handlebars, RenderContext, RenderError, html_escape};
+use tempdir::TempDir;
+use toml;
+
+/// toml_escape_helper quotes strings in templates when they are wrapped in
+/// {{#toml-escape <template-variable}}
+/// So if 'name' is "foo \"bar\"" then:
+/// {{name}} renders as 'foo "bar"'
+/// {{#toml-escape name}} renders as '"foo \"bar\""'
+pub fn toml_escape_helper(_: &Context,
+ h: &Helper,
+ _: &Handlebars,
+ rc: &mut RenderContext) -> Result<(), RenderError> {
+ if let Some(param) = h.param(0) {
+ let txt = param.value().as_string().unwrap_or("").to_owned();
+ let rendered = format!("{}", toml::Value::String(txt));
+ try!(rc.writer.write(rendered.into_bytes().as_ref()));
+ }
+ Ok(())
+}
+
+/// html_escape_helper escapes strings in templates using html escaping rules.
+pub fn html_escape_helper(_: &Context,
+ h: &Helper,
+ _: &Handlebars,
+ rc: &mut RenderContext) -> Result<(), RenderError> {
+ if let Some(param) = h.param(0) {
+ let rendered = html_escape(param.value().as_string().unwrap_or(""));
+ try!(rc.writer.write(rendered.into_bytes().as_ref()));
+ }
+ Ok(())
+}
+
+/// Trait to hold information required for rendering templated files.
+pub trait TemplateFile {
+ /// Path of the template output for the file being written.
+ fn path(&self) -> &Path;
+
+ /// Return the template string.
+ fn template(&self) -> CargoResult<String>;
+}
+
+/// TemplateFile based on an input file.
+pub struct InputFileTemplateFile {
+ input_path: PathBuf,
+ output_path: PathBuf,
+}
+
+impl TemplateFile for InputFileTemplateFile {
+ fn path(&self) -> &Path {
+ &self.output_path
+ }
+
+ fn template(&self) -> CargoResult<String> {
+ let mut template_str = String::new();
+ let mut entry_file = try!(File::open(&self.input_path).chain_error(|| {
+ human(format!("Failed to open file for templating: {}", self.input_path.display()))
+ }));
+ try!(entry_file.read_to_string(&mut template_str).chain_error(|| {
+ human(format!("Failed to read file for templating: {}", self.input_path.display()))
+ }));
+ Ok(template_str)
+ }
+}
+
+impl InputFileTemplateFile {
+ pub fn new(input_path: PathBuf, output_path: PathBuf) -> InputFileTemplateFile {
+ InputFileTemplateFile {
+ input_path: input_path,
+ output_path: output_path
+ }
+ }
+}
+
+/// An in memory template file for --bin or --lib.
+pub struct InMemoryTemplateFile {
+ template_str: String,
+ output_path: PathBuf,
+}
+
+impl TemplateFile for InMemoryTemplateFile {
+ fn path(&self) -> &Path {
+ &self.output_path
+ }
+
+ fn template(&self) -> CargoResult<String> {
+ Ok(self.template_str.clone())
+ }
+}
+
+impl InMemoryTemplateFile {
+ pub fn new(output_path: PathBuf, template_str: String) -> InMemoryTemplateFile {
+ InMemoryTemplateFile {
+ template_str: template_str,
+ output_path: output_path
+ }
+ }
+}
+
+pub enum TemplateDirectory{
+ Temp(TempDir),
+ Normal(PathBuf),
+}
+
+impl TemplateDirectory {
+ pub fn path(&self) -> &Path {
+ match *self {
+ TemplateDirectory::Temp(ref tempdir) => tempdir.path(),
+ TemplateDirectory::Normal(ref path) => path.as_path()
+ }
+ }
+}
+
+/// A listing of all the files that are part of the template.
+pub struct TemplateSet {
+ pub template_dir: Option<TemplateDirectory>,
+ pub template_files: Vec<Box<TemplateFile>>
+}
+
+// The type of template we will use.
+#[derive(Debug, Eq, PartialEq)]
+pub enum TemplateType {
+ GitRepo(String),
+ LocalDir(String),
+ Builtin
+}
+
+/// Given a repository string and subdir, determine if this is a git repository, local file, or a
+/// built in template. Git only supports a few schemas, so anything that is not supported is
+/// treated as a local path. The supported schemes are:
+/// "git", "file", "http", "https", and "ssh"
+/// Also supported is an scp style syntax: git@domain.com:user/path
+pub fn get_template_type<'a>(repo: Option<&'a str>,
+ subdir: Option<&'a str>) -> CargoResult<TemplateType> {
+ match (repo, subdir) {
+ (Some(repo_str), _) => {
+ if let Ok(repo_url) = Url::parse(&repo_str) {
+ let supported_schemes = ["git", "file", "http", "https", "ssh"];
+ if supported_schemes.contains(&repo_url.scheme()) {
+ Ok(TemplateType::GitRepo(repo_url.into_string()))
+ } else {
+ Ok(TemplateType::LocalDir(String::from(repo_str)))
+ }
+ } else {
+ Ok(TemplateType::LocalDir(String::from(repo_str)))
+ }
+ },
+ (None, Some(_)) => Err(human("A template was given, but no template repository")),
+ (None, None) => Ok(TemplateType::Builtin)
+ }
+}
+
+
+#[cfg(test)]
+mod test {
+ use std::collections::BTreeMap;
+ use handlebars::Handlebars;
+ use super::*;
+
+ #[test]
+ fn test_toml_escape_helper() {
+ let mut handlebars = Handlebars::new();
+ handlebars.register_helper("toml-escape", Box::new(toml_escape_helper));
+ let mut data = BTreeMap::new();
+ data.insert("name".to_owned(), "\"Iron\" Mike Tyson".to_owned());
+ let result = handlebars.template_render("Hello, {{#toml-escape name}}{{/toml-escape}}", &data).unwrap();
+ assert_eq!(result, "Hello, \"\\\"Iron\\\" Mike Tyson\"");
+ }
+
+ macro_rules! test_get_template_proto {
+ ( $funcname:ident, $url:expr ) => {
+ #[test]
+ fn $funcname() {
+ assert_eq!(get_template_type(Some($url), Some("foo")).unwrap(),
+ TemplateType::GitRepo($url.to_owned()));
+ assert_eq!(get_template_type(Some($url), Some("")).unwrap(),
+ TemplateType::GitRepo($url.to_owned()));
+ assert_eq!(get_template_type(Some($url), None).unwrap(),
+ TemplateType::GitRepo($url.to_owned()));
+ }
+ }
+ }
+
+ test_get_template_proto!(test_get_template_http, "http://foo.com/user/repo");
+ test_get_template_proto!(test_get_template_https, "https://foo.com/user/repo");
+ test_get_template_proto!(test_get_template_git, "git://foo.com/user/repo");
+ test_get_template_proto!(test_get_template_file, "file://foo.com/user/repo");
+ test_get_template_proto!(test_get_template_ssh, "ssh://user@foo.com/repo");
+ // SSH scp style repository access is not yet supported.
+ //test_get_template_proto!(test_get_template_ssh_scp_style, "git@foo.com:user/repo");
+
+ #[test]
+ fn test_get_template_type_git_repo_bad_proto_is_localdir() {
+ assert_eq!(get_template_type(Some("ftps://foo.com/user/repo"), None).unwrap(),
+ TemplateType::LocalDir("ftps://foo.com/user/repo".to_owned()));
+ }
+
+ #[test]
+ fn test_get_template_type_local_dir_abs() {
+ assert_eq!(get_template_type(Some("/foo/user/repo"), Some("foo")).unwrap(),
+ TemplateType::LocalDir("/foo/user/repo".to_owned()));
+ assert_eq!(get_template_type(Some("/foo/user/repo"), Some("")).unwrap(),
+ TemplateType::LocalDir("/foo/user/repo".to_owned()));
+ assert_eq!(get_template_type(Some("/foo/user/repo"), None).unwrap(),
+ TemplateType::LocalDir("/foo/user/repo".to_owned()));
+ }
+
+ // Windows paths can be parsed as URLs so make sure they are parsed as local directories.
+ #[test]
+ fn test_get_template_type_windows_path_is_localdir() {
+ assert_eq!(get_template_type(Some(r#"C:\foo\user\repo"#), None).unwrap(),
+ TemplateType::LocalDir(r#"C:\foo\user\repo"#.to_owned()));
+ assert_eq!(get_template_type(Some(r#"C:/foo/user/repo"#), None).unwrap(),
+ TemplateType::LocalDir(r#"C:/foo/user/repo"#.to_owned()));
+ }
+
+ #[test]
+ fn test_get_template_type_local_dir_rel() {
+ assert_eq!(get_template_type(Some("foo/user/repo"), Some("foo")).unwrap(),
+ TemplateType::LocalDir("foo/user/repo".to_owned()));
+ assert_eq!(get_template_type(Some("foo/user/repo"), Some("")).unwrap(),
+ TemplateType::LocalDir("foo/user/repo".to_owned()));
+ assert_eq!(get_template_type(Some("foo/user/repo"), None).unwrap(),
+ TemplateType::LocalDir("foo/user/repo".to_owned()));
+ }
+
+ #[test]
+ fn test_get_template_type_builtin() {
+ assert_eq!(get_template_type(None, None).unwrap(), TemplateType::Builtin);
+ }
+}
were making a library, we’d leave it off. This also initializes a new `git`
repository by default. If you don't want it to do that, pass `--vcs none`.
+You can also use your own template to scaffold cargo projects! See the
+[Templates](#templates) section for more details.
+
Let’s check out what Cargo has generated for us:
```shell
name = "hello_world"
version = "0.1.0"
authors = ["Your Name <you@example.com>"]
+
+[dependencies]
```
This is called a **manifest**, and it contains all of the metadata that Cargo
documentation](https://docs.travis-ci.com/user/languages/rust/) for more
information.
+# Templates
+
+Cargo uses the [handlebars](https://github.com/sunng87/handlebars-rust) library
+to compile the templates used to scaffold projects. By default, there are only
+two templates available, `bin` and `lib`. These are used by cargo to create the
+standard project structure.
+
+You can also specify other templates from which to scaffold your project. The
+`--template` argument to `cargo new` accepts either a path on your system, or a
+URL to a remote Git repository containing a project template.
+
+```
+# use the mytemplate template which is located in ~/.cargo/templates/mytemplate
+$ cargo new myproj --template ~/.cargo/mytemplates/mytemplate
+
+# download the template called mytemplate from your github package
+$ cargo new myproj --template http://github.com/you/mytemplate
+```
+
+If you have a collection of templates in a Git repository then you can use the
+`--template-subdir` option to specify the subdirectory containing the template
+you want to use.
+
+```
+# download the template project called mytemplates from your github package
+# and use the 'command-line-project' template.
+$ cargo new myproj --template http://github.com/you/mytemplate --template-subdir command-line-project
+```
+
+## Creating new templates
+
+A cargo template is just a folder containing one or more files. Usually, there
+is a `Cargo.toml` and a `src` directory. Each file in the template directory
+(aside from the contents of the .git directory) will be treated as a handlebars
+template. This means you can use handlebars variables wherever you want dynamic
+content, and cargo will render the proper values. Let's create a simple example.
+Create a new folder called `mytemplate`
+
+Add the following files:
+
+```toml
+# Cargo.toml
+[project]
+name = "{{name}}"
+version = "0.1.0"
+authors = [{{toml-escape author}}]
+```
+
+```rust
+// src/main.rs
+fn main() {
+ prinln!("This is the {{name}} project!");
+}
+```
+
+Upload this to a public git repository and anyone can now use it to start their
+projects with this command:
+
+```
+$ cargo new proj --template http://your/project/repo
+```
+
+## Available variables
+
+The variables available for use are:
+
+- `name`: the name of the project
+- `authors`: the toml formatted name of the project author
+
+In the future, more variables may be added. Suggestions welcome!
+
+## Available templating functions
+
+The available templating functions are:
+
+- `toml-escape`: Escapes a string for use in a TOML file.
+ file.
+- `html-escape`: Escapes a string for use in a HTML file.
+
+There is more documentation available on the [Handlebars
+website](http://handlebarsjs.com/) though keep in mind that [the Rust
+implementation of Handlebars](https://docs.rs/handlebars/0.24.1/handlebars/)
+isn't 100% compatible with the Javascript version.
+
# Further reading
Now that you have an overview of how to use cargo and have created your first crate, you may be interested in:
'--vcs:initialize a new repo with a given VCS:(git hg none)' \
'(-h, --help)'{-h,--help}'[show help message]' \
'--name=[set the resulting package name]' \
+ '--template[template from which to scaffold your project]' \
'(-q, --quiet)'{-q,--quiet}'[no output printed to stdout]' \
'(-v, --verbose)'{-v,--verbose}'[use verbose output]' \
'--color=:colorization option:(auto always never)' \
local opt__locate_project="$opt_mani -h --help"
local opt__login="$opt_common $opt_lock --host"
local opt__metadata="$opt_common $opt_feat $opt_mani $opt_lock --format-version --no-deps"
- local opt__new="$opt_common $opt_lock --vcs --bin --lib --name"
+ local opt__new="$opt_common $opt_lock --vcs --bin --lib --name --template"
local opt__owner="$opt_common $opt_lock -a --add -r --remove -l --list --index --token"
local opt__package="$opt_common $opt_mani $opt_lock $opt_jobs --allow-dirty -l --list --no-verify --no-metadata"
local opt__pkgid="${opt__fetch} $opt_pkg"
use cargo::util::ProcessBuilder;
use cargotest::process;
-use cargotest::support::{execs, paths};
+use cargotest::support::{execs, git, paths};
use hamcrest::{assert_that, existing_file, existing_dir, is_not};
use tempdir::TempDir;
existing_file());
}
+#[test]
+fn simple_template() {
+ let root = paths::root();
+ fs::create_dir_all(&root.join("home/.cargo/templates/testtemplate/src")).unwrap();
+ File::create(&root.join("home/.cargo/templates/testtemplate/Cargo.toml"))
+ .unwrap().write_all(br#"[package]
+name = "{{name}}"
+version = "0.0.1"
+authors = ["{{author}}"]
+"#).unwrap();
+ File::create(&root.join("home/.cargo/templates/testtemplate/src/main.rs"))
+ .unwrap().write_all(br#"
+fn main () {
+ println!("hello {{name}}");
+}
+ "#).unwrap();
+
+ assert_that(cargo_process("new").arg("--template-subdir").arg("testtemplate")
+ .arg("--template")
+ .arg(&root.join("home/.cargo/templates/"))
+ .arg("foo")
+ .env("USER", "foo"),
+ execs().with_status(0).with_stderr("\
+[CREATED] library `foo` project
+"));
+
+ assert_that(&paths::root().join("foo"), existing_dir());
+ assert_that(&paths::root().join("foo/Cargo.toml"), existing_file());
+ assert_that(&paths::root().join("foo/src/main.rs"), existing_file());
+
+ assert_that(cargo_process("build").cwd(&paths::root().join("foo")),
+ execs().with_status(0));
+ assert_that(&paths::root().join(&format!("foo/target/debug/foo{}",
+ env::consts::EXE_SUFFIX)),
+ existing_file());
+}
+
+#[test]
+fn git_template() {
+ let git_project = git::new("template1", |project| {
+ project
+ .file("Cargo.toml", r#"[package]
+name = "{{name}}"
+version = "0.0.1"
+authors = ["{{author}}"]
+ "#)
+ .file("src/main.rs", r#"
+ pub fn main() {
+ println!("hello world");
+ }
+ "#)
+ }).unwrap();
+
+ assert_that(cargo_process("new").arg("--template").arg(git_project.url().as_str())
+ .arg("foo")
+ .env("USER", "foo"),
+ execs().with_status(0));
+
+ assert_that(&paths::root().join("foo"), existing_dir());
+ assert_that(&paths::root().join("foo/Cargo.toml"), existing_file());
+ assert_that(&paths::root().join("foo/src/main.rs"), existing_file());
+
+ assert_that(cargo_process("build").cwd(&paths::root().join("foo")),
+ execs().with_status(0));
+ assert_that(&paths::root().join(&format!("foo/target/debug/foo{}",
+ env::consts::EXE_SUFFIX)),
+ existing_file());
+}
+
#[test]
fn both_lib_and_bin() {
let td = TempDir::new("cargo").unwrap();